""" A bitcoind proxy that allows instrumentation and canned responses
"""
from flask import Flask, request
from bitcoin.rpc import JSONRPCError
from bitcoin.rpc import RawProxy as BitcoinProxy
from cheroot.wsgi import Server
from cheroot.wsgi import PathInfoDispatcher

import decimal
import flask
import json
import logging
import threading


class DecimalEncoder(json.JSONEncoder):
    """By default json.dumps does not handle Decimals correctly, so we override it's handling
    """
    def default(self, o):
        if isinstance(o, decimal.Decimal):
            return "{:.8f}".format(float(o))
        return super(DecimalEncoder, self).default(o)


class BitcoinRpcProxy(object):
    def __init__(self, bitcoind, rpcport=0):
        self.app = Flask("BitcoindProxy")
        self.app.add_url_rule("/", "API entrypoint", self.proxy, methods=['POST'])
        self.rpcport = rpcport
        self.mocks = {}
        self.mock_counts = {}
        self.bitcoind = bitcoind
        self.request_count = 0

    def _handle_request(self, r):
        brpc = BitcoinProxy(btc_conf_file=self.bitcoind.conf_file)
        method = r['method']

        # If we have set a mock for this method reply with that instead of
        # forwarding the request.
        if method in self.mocks and type(self.mocks[method]) == dict:
            ret = {}
            ret['id'] = r['id']
            ret['error'] = None
            ret['result'] = self.mocks[method]
            self.mock_counts[method] += 1
            return ret
        elif method in self.mocks and callable(self.mocks[method]):
            self.mock_counts[method] += 1
            return self.mocks[method](r)

        try:
            reply = {
                "result": brpc._call(r['method'], *r['params']),
                "error": None,
                "id": r['id']
            }
        except JSONRPCError as e:
            reply = {
                "error": e.error,
                "code": -32603,
                "id": r['id']
            }
        self.request_count += 1
        return reply

    def proxy(self):
        r = json.loads(request.data.decode('ASCII'))

        if isinstance(r, list):
            reply = [self._handle_request(subreq) for subreq in r]
        else:
            reply = self._handle_request(r)

        response = flask.Response(json.dumps(reply, cls=DecimalEncoder))
        response.headers['Content-Type'] = 'application/json'
        return response

    def start(self):
        d = PathInfoDispatcher({'/': self.app})
        self.server = Server(('0.0.0.0', self.rpcport), d)
        self.proxy_thread = threading.Thread(target=self.server.start)
        self.proxy_thread.daemon = True
        self.proxy_thread.start()

        # Now that bitcoind is running on the real rpcport, let's tell all
        # future callers to talk to the proxyport. We use the bind_addr as a
        # signal that the port is bound and accepting connections.
        while self.server.bind_addr[1] == 0:
            pass
        self.rpcport = self.server.bind_addr[1]
        logging.debug("BitcoinRpcProxy proxying incoming port {} to {}".format(self.rpcport, self.bitcoind.rpcport))

    def stop(self):
        self.server.stop()
        self.proxy_thread.join()
        logging.debug("BitcoinRpcProxy shut down after processing {} requests".format(self.request_count))

    def mock_rpc(self, method, response=None):
        """Mock the response to a future RPC call of @method

        The response can either be a dict with the full JSON-RPC response, or a
        function that returns such a response. If the response is None the mock
        is removed and future calls will be passed through to bitcoind again.

        """
        if response is not None:
            self.mocks[method] = response
            self.mock_counts[method] = 0
        elif method in self.mocks:
            del self.mocks[method]
